# Copyright 2011 Google Inc. All Rights Reserved.
# Author: sdd@google.com (Salvatore Domenick Desiano)
#
# Helper functions and variables for GLP development and deployment scripts.
#
# GLP uses a formal code process review built on Mercurial and Rietveld, but
# similar to that used by Git users at Google. The developer works in their own,
# private Mercurial clone, creating an additional clone for each changeset to be
# reviewed and pushed to the global repository. Moreover, only the final,
# reviewed code is pushed to the global repository, allowing the developer to
# make commits while developing without fear of anyone seeing their dirty
# underwear.
#
# A few terms:
#   Edit Clone - a Mercurial clone in which the developer works directly,
#     committing as needed, in accordance with their personal development
#     preferences.
#   Issue Clone - a Mercurial clone, tied to an edit clone, used to facilitate
#     Rietveld code reviews and containing exactly one changeset per review
#     cycle.
#   Push Clone - a Mercurial clone, tied to an issue clone, that exists only
#     while the final result of reviewed code is being pushed to the global
#     Mercurial repository.
#   Checkout - a directory structure containing one or more edit clones and all
#     related issue and push clones. In theory, each edit clone could live in
#     its own checkout, but caching makes it much faster to create a new edit
#     clone within a checkout than a new checkout.
#
# To create your first checkout (or if you've lost your copy of glp-checkout):
#   1) hg clone https://glp-google.googlecode.com/hg bootstrap
#   2) bootstrap/scripts/glp-checkout --bootstrap CheckoutDirectoryName
#   3) rm -fr bootstrap
#   4) Continue with step 2, below.
#
# The basic development cycle:
#   1) Create a checkout and the first edit clone:
#        src/scripts/glp-checkout CheckoutDirectoryName EditCloneName1
#   2) As needed:
#       - Edit files in your edit clone(s), committing as you go.
#       - Create additional edit clones:
#           src/scripts/glp-clone-create EditCloneNameN
#   3) When you have code ready for review, collect it in a single edit clone
#      and create an Rietveld issue for review:
#        src/scripts/glp-issue-create reviewer@foo.bar 'Issue description'
#   4) Wait for review (you will be notified by e-mail when it is complete).
#   5) In response to the review:
#       - Review Reitveld comments (http://google-glp-review.appspot.com).
#       - Edit files in CheckoutDirectoryName/ReviewEditCloneName, committing as
#         you go.
#       - Respond to _all_ Rietveld comments.
#       - Publish your comments.
#   6) Update the Rietveld issue: src/scripts/glp-issue-update
#   7) Repeat steps 4-6 until you have _both_ an LGTM and a LRTM.
#   8) Push the changes to the global repository: src/scripts/glp-issue-push
#   9) Delete the edit clone, if desired:
#        src/scripts/glp-clone-delete ReviewEditCloneName
#   10) Close the issue using the Rietveld web interface.
#   11) Repeat steps 2-10 as desired.
#   12) Delete the entire checkout: rm -fr CheckoutDirectoryName
#   13) Start again!
#
# Notes:
#   - After dropping or pushing an issue, you must close it, manually, using the
#       Rietveld web interface.
#   - Be sure to delete clones using glp-clone-delete, even if their contents
#       are not put up for review.
#   - Edit clones can be used for review, developing, or both. Some people
#       develop many files in a clone and copy them to other edit clones, a few
#       at a time, for review. Others only develop a few files at a time in each
#       edit clone and use that clone for review.
#   - If you decide not to push an issue, you can drop the issue entirely with
#       src/scripts/glp-issue-drop. This does not affect the edit clone, but
#       disconnects it from Rietveld so that you must create a new issue to get
#       the code in that clone reviewed. WARNING: this will delete the issue
#       clone and all of it s history.
#   - If you want to use an edit clone for another review, you must follow
#       glp-issue-push with glp-issue-drop. WARNING: this will delete the issue
#       clone and all of its history.
#
#
# ==== IMPLEMENTATION DETAILS ====
#
#   "Mercurial", "Direct", and "Indirect" Synchronization
#
# The GLP review process is built on two invented concepts: direct and indirect
# synchronization.
#
# Mercurial uses "pull" and "push" to synchronize two clones. The only
# difference between the two is which clone you are in when you execute the
# command. The only reason they both exist is that it is sometimes not possible
# to be "in" a remote clone. In this discussion, all clones are local, so "push"
# and "pull" between "upstream" and "downstream" simply become "sync" between
# "src" and "dst".
#
# A "direct" synchronization acts much like a traditional Mercurial push or
# pull; synchronizing src and dst ensures that dst contains all of src's
# changesets. Unlike pull/push, however, a direct sync is quite forceful -- all
# changesets are merged, resolved, and committed automatically. If not done
# carefully, this can result in dst containing committed files that include diff
# markup.
#
# An "indirect" synchronization is very different; there is no actual Mercurial
# relationship between the two clones. Instead, an indirect sync modifies the
# _files_ in dst to match src, and then commits them. In this way, an indirect
# sync still causes the files at the tip of the two clones to match, but the
# changesets, changeset, comments, merges, and histories that got them there are
# unrelated.
#
#
#   Implementation Notes
#
# The GLP code review process is built on a "checkout", which contains an
# "upstream" clone, one or more "edit" clones, zero or one "issue" clones per
# edit clone, and, briefly, one "push" clone per issue clone:
#
#   CheckoutDirectoryName
#     glp.checkout.rc
#     EditClone1
#     EditClone2
#     ...
#     EditCloneN
#     review
#       upload.py
#       state.1
#       ...
#       state.N
#       IssueClone1   [if an issue has been created]
#       ...                         "
#       IssueCloneN                 "
#       message.1                   "
#       ...                         "
#       message.N                   "
#       PushClone     [while code is being pushed]
#
# The upstream clone is a faithful cache of the global repository used. It is
# updated before each use, but this is still faster than working directly
# against the global repository each time.
#
# Edit clones are used by the developer to make and test changes, making commits
# according to their personal development preferences. For speed reasons, this
# clone is cloned from the upstream clone, but is immediately changed so that
# pulls come from the global repository. The only time an edit clone is used as
# the source of a synchronization is when indirectly synchronizing an issue
# clone.
#
# Issue clones are used for reviewing the code in a single edit clone.  When a
# developer is ready to have code reviewed, glp-issue-create:
#  - creates an issue clone by cloning the upstream clone,
#  - synchronizes the issue clone to match the corresponding edit clone,
#  - creates a changeset in the issue clone from the code to be reviewed, and
#  - uploads the code to Rietveld for review
# As the review cycle continues, more changes are made to the edit clone, pushed
# "indirectly" to the issue clone, and uploaded for review. Each review cycle
# results in any number of edit clone changesets, but exactly one issue clone
# changeset.
#
# Push clones are used for pushing reviewed code to the global repository. When
# code is ready for pushing, glp-issue-push:
#  - creates a push clone by cloning the upstream clone,
#  - synchronizes the push clone to match the specified issue clone,
#  - creates a changeset in the push clone to be pushed,
#  - pushes that single changeset to the global repository,
#  - pulls the changeset into the upstream clone, and
#  - deletes the push clone.
# While the upstream clone could be used directly, the process is more robust to
# failure if the upstream clone is only involved in Mercurial (rather than
# direct or indirect) synchronizatoin.
#
# Using this mechanism, the developer can commit freely, the code review process
# is fully recorded using Rietveld, and the global repository contains only the
# completed, reviewed code.


# Global variables read from glp.*.rc and set (and set to read-only) in
# glp::initialize_checkout_variables():
#   URL of upstream Mercurial repository
declare HG_URL
#   URL of Rietveld code review server
declare RIETVELD_URL
#   Developer's institutional e-mail address
declare ORG_USER
#   E-mail address for code review notifications
declare REVIEW_EMAIL


# Global variables set in (and set to read-only) in
# glp::initialize_checkout_variables()):
#   Locations (wrt to the specified edit clone):
declare EDIT_CLONE      # files for editing by developer.
declare ISSUE_CLONE     # files for upload to Rietveld.
declare PUSH_CLONE      # files for pushing to global repository.
declare UPSTREAM_CLONE  # cache of the global repository.
declare SCRIPT_DIR      # location of code review scripts
#   Issue Data (wrt the specified edit clone):
declare ISSUE_STATE          # issue state: empty, active, or complete
declare ISSUE_NUMBER         # number of corresponding Rietveld issue
declare ISSUE_BASE_REV       # upstream clone rev of issue base
declare ISSUE_REV            # issue clone rev of most recent patch set
declare ISSUE_STATE_PATH     # disk cache of issue data (except description)
declare ISSUE_MESSAGE_PATH  # file containing issue's description
#   Rietveld Data:
declare RIETVELD_UPLOAD_SCRIPT       # uploads data to Rietveld server.
declare RIETVELD_PARAMS              # parameters for all Rietveld scripts.


#######################################
# Prints a warning to STDERR with "warning: " prepended.
#
# Arguments:
#   message - the warning to print
# Returns:
#   0 (success)
#######################################
function glp::warn() {
  local -r message="${1}"
  echo "warning: ${message}" 2>&1
}


#######################################
# Prints a warning to STDERR with the script name prepended and exits with
# status 3. See http://tldp.org/LDP/abs/html/exitcodes.html for an explation of
# the 3.
#
# Arguments:
#   message - the error to print
# Returns:
#   n/a (exits)
#######################################
function glp::die() {
  local -r message="${1}"
  if [[ -z "${message}" ]]; then message='unspecified error'; fi
  echo "$0: ${message}" 2>&1
  exit 3
}


#######################################
# Reads the contents of a file, linebreaks and all, into a variable.
#
# Arguments:
#   path - location of file
#   variable - name of variable
# Returns
#   0 or n/a (exits)
#######################################
function glp::read_file() {
  local -r path="${1}"
  local -r variable="${2}"
  if [[ ! -e "${path}" ]]; then
    glp::die "unable to read ${path}"
  fi
  # The exit value for read seems to be reversed. Can't find
  # documentation on why.
  read -r -d '' "${variable}" < "${path}" && glp::die 'failure.'
}


#######################################
# Tries to create a directory using mkdir and die on failure. The directory
# already existing is considered a failure.
#
# Arguments:
#   directory - the full path of the directory to create
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::mkdir_or_die() {
  local -r dir="${1}"
  mkdir "${dir}" || glp::die "unable to create ${dir}"
}


#######################################
# Tries to enter a directory using cd and dies on failure.
#
# Arguments:
#   directory - the full path of the directory to enter
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::cd_or_die() {
  local -r dir="${1}"
  cd "${dir}" || glp::die "unable to enter ${dir}"
}


#######################################
# Executes "cd -" and dies on failure.
#
# Arguments:
#   directory - the full path of the directory to enter
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::cd_oldpath_or_die() {
  local -r last_dir="${1}"
  cd - > /dev/null || glp::die "unable to exit ${dir}"
}


#######################################
# Tries to source a file and warn on failure. Dies if no script is specified.
#
# Arguments:
#   script - the full path of the file to source
# Returns:
#   0 (success) or 3 (failure)
#######################################
function glp::source_or_warn() {
  local -r script="${1}"
  if [[ -z "${script}" ]]; then
    glp::die 'glp::source_or_warn: no script specified'
    return 3
  elif [[ ! -e "${script}" ]]; then
    glp::warn "${script} does not exist"
    return 3
  elif source "${script}"; then
    return 0
  else
    glp::warn "unable to source ${script}"
    return 3
  fi
}


#######################################
# Calculates the revision ID of the tip of the specified Mercurial clone. Dies
# if no clone is found at the specified location.
#
# Arguments:
#   clone - The path of any file or directory in the clone
#   variable - The name of the variable in which to store the ID.
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::calculate_tip() {
  local -r clone="${1}"
  local -r variable="${2}"
  local revision
  glp::cd_or_die "${clone}"
  readonly revision="$(hg log --template '{node|short}' --limit 1)"
  if [[ -z "${revision}" ]]; then
    glp::die 'unable to calculate tip revision';
  fi
  eval "${variable}='${revision}'"
  glp::cd_oldpath_or_die "${clone}"
  eval "readonly ${variable}"
  return 0
}


#######################################
# "Mercurially" synchronizes dst to match src. One of src or dst must be
# local. Dies on failure. See comments at the top of this file for a description
# of synchronization types.
#
# Arguments:
#   src - Clone from which changesets will be pulled.
#   dst - Clone into which changesets will be pushed.
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::hg_sync() {
  local -r src="${1}"
  local -r dst="${2}"
  if cd "${src}"; then
    hg push --quiet "${dst}" || glp::die "unable to push from ${src} to ${dst}"
    glp::cd_oldpath_or_die "${src}"
  elif cd "${dst}"; then
    hg pull --quiet "${src}" || glp::die "unable to pull from ${src} to ${dst}"
    glp::cd_oldpath_or_die "${dst}"
  else
    glp::die "unable to enter ${src} or ${dst}"
  fi
  return 0
}


#######################################
# "Directly" synchronizes dst to match src by pushing changesets, updating,
# merging, resolving, and committing as needed. dst must be local. Dies on
# failure. See comments at the top of this file for a description of
# synchronization types.
#
# Note: this function is a blunt tool -- if used incorrectly, it will commit
# files in dst that contain merge annotations.
#
# Arguments:
#   src - Clone from which changesets will be pulled.
#   dst - Clone into which changesets will be pushed. Must be local.
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::direct_sync() {
  local -r src="${1}"
  local -r dst="${2}"
  glp::cd_or_die "${dst}"
  echo '  pulling changeset(s) from source...'
  hg pull --update --quiet "${src}" || glp::die "failed."
  if [[ "$(echo $(hg summary | grep 'update: .* (merge)$' | wc -l))" \
    != 0 ]]; then
    echo '  merging destination heads...'
    hg merge --force --quiet 2>/dev/null
    echo '  resolving destination conflicts...'
    hg resolve -am --quiet || glp::die 'failed.'
    echo '  committing destination merge...'
    hg commit --quiet -m 'source: glp::direct_sync()' || die 'failed.'
  fi
  glp::cd_oldpath_or_die "${dst}"
}


#######################################
# Verifies that clone is completely up to date and has no outstanding changes to
# be committed. Assumes that the default clone specified in .hg/hgrc is up to
# date.
#
# Arguments:
#   clone - the root directory of the clone to be verified.
#   complaint - message used if assertion fails.
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::direct_assert_up_to_date() {
  local -r clone="${1}"
  local -r message="${2}"
  glp::cd_or_die "${clone}"
  if [[ $(echo $(hg status | wc -l)) != 0 ]]; then
    glp::die "${message} (${clone} has uncommitted changes)"
  fi
  if [[ $(echo $(hg incoming | echo $(wc -l))) != 3 ]]; then
    glp::die "${message} (${clone} has pending changesets)"
  fi
  if [[ $(hg summary | grep '^update: ') != "update: (current)" ]]; then
    glp::die "${message} (${clone} has pending updates)"
  fi
  glp::cd_oldpath_or_die "${clone}"
  return 0
}


#######################################
# "Indirectly" synchronizes dst to match src by modifying dst so that its tip
# matches the tip of src (i.e., diff -r ${src} ${dst} would return nothing), but
# does not push actual changesets from src to dst. src and dst must be
# local. This allows the developer to push changes upstream without exposing
# intermediate commits (i.e., the developer's dirty underwear).
#
# Note: this function is a blunt tool -- if used incorrectly, it will hide
# changes in dst by committing over them.
#
# Arguments:
#   src - Root of clone from which files will be pulled. Must be local.
#   dst - Root of clone from which files will be pushed. Must be local.
#   message - Message for the new dst changeset. Optional. Ignored if
#     no changset is required (i.e. src and dst already match). If
#     needed and not present, Mercurial will prompt the user for a
#     message using the traditional prompt window.
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::indirect_sync() {
  local -r src="${1}"
  local -r dst="${2}"
  local -r message="${3}"
  local src_root
  local dst_root
    
  # Make sure that src and dst are different.
  glp::cd_or_die "${src}"
  src_root=$(hg root)
  if [[ $? != 0 ]]; then glp::die "unable to determine root of ${src}"; fi
  readonly src_root
  glp::cd_oldpath_or_die "${src}"
  glp::cd_or_die "${dst}"
  dst_root=$(hg root)
  if [[ $? != 0 ]]; then glp::die "unable to determine root of ${dst}"; fi
  readonly dst_root
  if [[ "${src_root}" == "${dst_root}" ]]; then
    glp::die "src and dst are both ${src_root}"
  fi

  # Push the src files into dst and addremove/commit as needed.
  echo '  updating files to match edit clone...'
  rsync --quiet --checksum --recursive --links --perms --executability --owner \
    --group --times --delete --exclude=.hg "${src}/" "${dst}" || \
    glp::die 'failed.'
  echo "  updating file indices..."
  hg addremove || glp::die 'failed.'
  if [[ $(echo $(hg status | wc -l)) == 0 ]]; then
    echo "  no changes to commit, skipping step..."
  else
    echo "  committing upstream changeset(s) and edit clone modifications..."
    if [[ -z "${message}" ]]; then
      hg commit || glp::die 'failed.'
    else
      hg commit -m "${message}" || glp::die 'failed.'
    fi
  fi
  glp::cd_oldpath_or_die "${dst}"
}


#######################################
# Writes current state of Rietveld issue review to ${ISSUE_STATE_PATH}.
#
# Arguments (see corresponding global variables, above):
#   issue_state, issue_number, issue_base_rev, issue_rev
# Globals Used (see top of file for descriptions):
#   ISSUE_STATE_PATH
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::write_issue_state() {
  local -r updated_issue_state="${1}"
  local -r updated_issue_number="${2}"
  local -r updated_issue_base_rev="${3}"
  local -r updated_issue_rev="${4}"
  cat > "${ISSUE_STATE_PATH}" <<EOF
ISSUE_STATE='${updated_issue_state}'
ISSUE_NUMBER=${updated_issue_number}
ISSUE_BASE_REV='${updated_issue_base_rev}'
ISSUE_REV='${updated_issue_rev}'
EOF
  if [[ "$?" -ne 0 ]]; then glp::die "unable to update ${ISSUE_STATE_PATH}"; fi
}


#######################################
# Reads issue state from disk.
#
# Arguments:
#   None.
# Globals Used:
#   ISSUE_STATE_PATH, ISSUE_MESSAGE_PATH
# Globals Set:
#   ISSUE_STATE, ISSUE_NUMBER, ISSUE_BASE_REV, ISSUE_REV
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::read_issue_state() {
  # In bootstrap mode, there's no data to read or review directory to verify.
  if [[ -z "${ISSUE_STATE_PATH}" ]]; then return 0; fi

  # Default to empty.
  ISSUE_STATE='empty'
  
  # Read the issue state file.
  if [[ -e "${ISSUE_STATE_PATH}" ]]; then
    source "${ISSUE_STATE_PATH}" || \
      glp::die "unable to source ${ISSUE_STATE_PATH}"
  fi
  readonly ISSUE_STATE
  readonly ISSUE_NUMBER
  readonly ISSUE_BASE_REV
  readonly ISSUE_REV
}


#######################################
# Verifies that issue state files match the state itself.
#
# Arguments:
#   None.
# Globals Used:
#   ISSUE_STATE, ISSUE_STATE_PATH, ISSUE_MESSAGE_PATH, ISSUE_NUMBER,
#   ISSUE_BASE_REV, ISSUE_REV
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::verify_issue_files() {
  if [[ ${ISSUE_STATE} == 'empty' ]]; then
    if [[ "${ISSUE_NUMBER}" != "" ]]; then
      glp::die "internal error: ISSUE_NUMBER set for ${ISSUE_STATE} issue"
    fi
    if [[ "${ISSUE_BASE_REV}" != "" ]]; then
      glp::die "internal error: ISSUE_BASE_REV set for ${ISSUE_STATE} issue"
    fi
    if [[ "${ISSUE_REV}" != "" ]]; then
      glp::die "internal error: ISSUE_REV set for ${ISSUE_STATE} issue"
    fi
    if [[ -e "${ISSUE_MESSAGE_PATH}" ]]; then
      glp::die "message file ${ISSUE_MESSAGE_PATH} exists for ${ISSUE_STATE} \
issue"
    fi
    if [[ -e "${ISSUE_CLONE}" ]]; then
      glp::die "issue clone ${ISSUE_CLONE} exists for ${ISSUE_STATE} issue"
    fi
  elif [[ ${ISSUE_STATE} == 'active' || ${ISSUE_STATE} == 'complete' ]]; then
    if [[ "${ISSUE_NUMBER}" == "" ]]; then
      glp::die "internal error: ISSUE_NUMBER set for ${ISSUE_STATE} issue"
    fi
    if [[ "${ISSUE_BASE_REV}" == "" ]]; then
      glp::die "internal error: ISSUE_BASE_REV set for ${ISSUE_STATE} issue"
    fi
    if [[ "${ISSUE_REV}" == "" ]]; then
      glp::die "internal error: ISSUE_REV set for ${ISSUE_STATE} issue"
    fi
    if [[ ! -e "${ISSUE_MESSAGE_PATH}" ]]; then
      glp::die "message file ${ISSUE_MESSAGE_PATH} exists for ${ISSUE_STATE} \
issue"
    fi
    if [[ ! -e "${ISSUE_CLONE}" ]]; then
      glp::die "issue clone ${ISSUE_CLONE} exists for ${ISSUE_STATE} issue"
    fi
  else
    glp::die 'internal error.'
  fi
  return 0
}


#######################################
# Adds a prefix and suffix to a filename, leaving the path intact.
#
# Arguments:
#   path - full path of the file.
#   prefix - prefix to add.
#   dest - variable in which to place modified path.
# Returns:
#   0 (success)
#######################################
function glp::prefixed_path() {
  local -r path="${1}"
  local -r prefix="${2}"
  local -r dest="${3}"
  local -r dir="$(dirname "${path}")"
  local -r file="$(basename "${path}")"
  eval "readonly ${dest}='${dir}/${prefix}${file}~'"
}


#######################################
# Backs up the current issue state and message, forgets about the issue, and
# prompts the user to delete the issue clone. Does not change edit clone in any
# way.
#
# Arguments:
#   None.
# Globals Used (see top of file for descriptions):
#   ISSUE_STATE_PATH, ISSUE_NUMBER, ISSUE_MESSAGE_PATH
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::reset_issue_state() {
  local state_backup
  local message_backup
  local clone_backup
  local -r prefix="${ISSUE_NUMBER}."
  glp::prefixed_path "${ISSUE_STATE_PATH}" "${prefix}" 'state_backup'
  glp::prefixed_path "${ISSUE_MESSAGE_PATH}" "${prefix}" 'message_backup'
  glp::prefixed_path "${ISSUE_CLONE}" "${prefix}" 'clone_backup'
  if [[ "${ISSUE_STATE}" == 'empty' ]]; then
    glp::die "attempt to reset review state from issue state ${ISSUE_STATE}"
  fi
  if [[ -e "${state_backup}" ]]; then
    glp::die "refusing to overwrite ${state_backup}"
  fi
  if [[ -e "${message_backup}" ]]; then
    glp::die "refusing to overwrite ${message_backup}"
  fi
  if [[ -e "${clone_backup}" ]]; then
    glp::die "refusing to overwrite ${clone_backup}"
  fi
  mv "${ISSUE_STATE_PATH}" "${state_backup}" || glp::die 'failed.'
  mv "${ISSUE_MESSAGE_PATH}" "${message_backup}" || glp::die 'failed.'
  mv "${ISSUE_CLONE}" "${clone_backup}" || glp::die 'failed.'
  glp::warn "reccomend deleting ${clone_backup}"
  return 0
}


#######################################
# Loads GLP settings, reads the current issue review state, and initializes
# variables pointing to various important files and directories. If ORG_USER is
# already set, returns without error or side effect. Note that the variables are
# set with respect to the specified edit clone, only.
#
# Settings are read from global
# (${checkout}/review/upstream/scripts/glp.global.rc), user (${HOME}/.glprc),
# and checkout (${checkout}/glp.checkout.rc) files, in that order. In this way,
# checkout-specifc settings override user settings, which override global
# settings.
#
# See the top of this file for more information on the meanings of these
# variables.
#
# Arguments:
#   mode - 'bootstrap' or 'natural'. Bootstrap mode sets up the environment when
#     the script is being run out of a Mercurial clone rather than out of a
#     clone within a checkout (this would otherwise generate an error). In
#     bootstrap mode, variables requiring a checkout are left empty.
#   edit_clone_name - Name of edit clone. Optional. Default is derived from
#     current directory. Required in bootstrap mode.
#   checkout_root - Checkout root. Optional. Default is derived from current
#     directory. Required in bootstrap mode.
# Globals Used:
#   HOME
# Globals Set:
#   EDIT_CLONE, HG_URL, ISSUE_BASE_REV, ISSUE_CLONE, ISSUE_MESSAGE_PATH,
#   ISSUE_NUMBER, ISSUE_REV, ISSUE_STATE, ISSUE_STATE_PATH, ORG_USER,
#   PUSH_CLONE, REVIEW_EMAIL, RIETVELD_PARAMS, RIETVELD_UPLOAD_SCRIPT,
#   RIETVELD_URL, SCRIPT_DIR, UPSTREAM_CLONE
# Returns:
#   0 (success) or n/a (exits)
#######################################
function glp::initialize_checkout_variables() {
  local edit_clone_name="${1}"
  local checkout_root="${2}"
  local -r pwd_clone="$(hg root 2>/dev/null)"
  local mode
  local review_root

  # If checkout_root isn't set, we try to guess
  if [[ ! -z "${checkout_root}" ]]; then
    readonly checkout_root
  elif [[ ! -z "${pwd_clone}" ]]; then
    readonly checkout_root="$(cd "${pwd_clone}/.." && pwd && cd - > /dev/null)"
  elif [[ -e "${PWD}/review/upstream" ]]; then
    readonly checkout_root="${PWD}"
  fi

  # If edit_clone_name isn't set, we try to guess
  if [[ -z "${edit_clone_name}" ]]; then
    readonly edit_clone_name="$(echo "${pwd_clone}" | sed 's/^.*\///')"
  else
    readonly edit_clone_name
  fi
  readonly EDIT_CLONE="${checkout_root}/${edit_clone_name}"

  # Figure out where we're being run out of.
  # TODO(sdd): Fix these two lines after moving out of experimental.
  glp::warn "MANGLING ROOTS AND SCRIPTS FOR EXPERIMENTAL PURPOSES"
  readonly SCRIPT_DIR="$(cd "$(dirname "${0}")" && hg root && cd - > \
    /dev/null)/experimental/sdd/review-scripts"

  # Calculate derived values.
  readonly review_root="${checkout_root}/review"
  readonly ISSUE_CLONE="${review_root}/${edit_clone_name}.issue"
  readonly PUSH_CLONE="${review_root}/${edit_clone_name}.push"
  readonly UPSTREAM_CLONE="${review_root}/upstream"
  readonly ISSUE_STATE_PATH="${review_root}/${edit_clone_name}.state.sh"
  readonly ISSUE_MESSAGE_PATH="${review_root}/${edit_clone_name}.message.txt"
  readonly RIETVELD_UPLOAD_SCRIPT="${review_root}/upload.py"

  # Read rc files and set RIETVELD_PARAMS unless set in the rc files.
  glp::source_or_warn "${SCRIPT_DIR}/glp.global.rc"
  if [[ ! -e "${HOME}/.glprc" ]]; then
    glp::die "${HOME}/.glprc does not exist. consider using \
${SCRIPT_DIR}/glp.user.template.rc as a template"
  fi
  glp::source_or_warn "${HOME}/.glprc"
  # ORG_USER must (and can only) be set in .glprc
  readonly ORG_USER
  if [[ -z "${ORG_USER}" ]]; then
    glp::die "ORG_USER not set in ${HOME}/.glprc"
  fi
  # Checkout-specific rc only exists if checkout_root exists.
  if [[ -e "${checkout_root}" ]]; then
    glp::source_or_warn "${checkout_root}/glp.checkout.rc"
  fi
  readonly RIETVELD_URL
  readonly REVIEW_EMAIL
  readonly HG_URL
  if [[ -z "${RIETVELD_PARAMS}" ]]; then
    RIETVELD_PARAMS="--assume_yes --email=${ORG_USER} \
      --cc=${REVIEW_EMAIL} --server=${RIETVELD_URL} --send_mail"
  fi
  readonly RIETVELD_PARAMS

  # Read the issue state data, if it is available
  glp::read_issue_state

  return 0
}


#######################################
# Perform the housekeeping we always do before we create, update, or push issue
# code.
#
# Arguments:
#   rev - Variable for storing new tip of issue clone. See glp::calculate_tip().
#   message - Commit message for updates to issue clone. Optional (see
#     glp::indirect_sync()),
# Globals Used (see top of file for descriptions):
#   EDIT_CLONE, ISSUE_CLONE, HG_URL, UPSTREAM_CLONE
# Returns:
#   None.
#######################################
function glp::update_review_clones() {
  local -r rev="${1}"
  local -r message="${2}"
  echo '  updating upstream clone from global repository...'
  glp::hg_sync "${HG_URL}" "${UPSTREAM_CLONE}"
  echo '  updating issue clone from upstream clone...'
  glp::direct_sync "${UPSTREAM_CLONE}" "${ISSUE_CLONE}"
  echo '  verifying that edit clone is up to date...'
  glp::direct_assert_up_to_date "${EDIT_CLONE}" \
    'refusing to update issue clone'
  echo '  updating issue clone from edit clone...'
  glp::indirect_sync "${EDIT_CLONE}" "${ISSUE_CLONE}" "${message}"
  echo "  collecting issue revision information..."
  glp::calculate_tip "${ISSUE_CLONE}" "${rev}"
}


# Local Variables:
# mode: shell-script
# End:
